<?php
use frontend\modules\yii2\widgets\layouts\markdown\Markdown;

$this->title = 'Web页面请求控制器使用教程';
$this->registerMetaTag([
    'name' => 'keywords',
    'content' => 'yii,yii2,框架,应用结构,web,controller,页面,请求,浏览器,控制器,使用,教程'
]);
$this->registerMetaTag([
    'name' => 'description',
    'content' => 'Web类型的控制器是专门处理用户在浏览器上发起的页面请求的控制器类型'
]);
$this->registerMetaTag([
    'name' => 'author',
    'content' => 'chenzhiwei'
]);

$html = [
    "概述" => <<<HTML
# Web页面请求控制器使用教程
Web类型的控制器是专门处理用户在浏览器上发起的页面请求的控制器类型，拥有完善的cookie、session用户信息处理机制，有强大的权限控制能力，是MVC的重要组成部分。
HTML,
    "父类" => <<<HTML
## 父类 id=father-class
如果你想创建Web类的控制器，那你的控制器应该继承 [yii\web\Controller](https://www.yiichina.com/doc/api/2.0/yii-web-controller)，举例如下：
```php
namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
 ... ...
}
``` 

HTML,
    "默认控制器" => <<<HTML
## 默认控制器 id=default-controller
当你的url并没有路由信息的时候，Yii提供了一个默认的控制器，这个默认控制器的定义在应用主体中。由于应用主体在入口文件中完成实例化，因此我们可以通过入口文件`web/index.php`找到应用主体类：
```php
... ...
// 此处完成应用主体的实例化
(new yii\web\Application(\$config))->run();

```
而应用主体类的第一个属性就是对默认控制器的定义：
```php
class Application extends \yii\base\Application
{
    /**
     * @var string the default route of this application. Defaults to 'site'.
     */
    public \$defaultRoute = 'site';
    
    ... ...
}
```
因此如果你想改变默认控制器，则可以在实例化应用主体的时候通过`\$config`传入新的默认控制器，如：
```php
... ...
\$config = array_merge(\$config, ["defaultRoute" => "bootstrap"]);

// 此处完成应用主体的实例化
(new yii\web\Application(\$config))->run();

```

HTML,
    "csrf" => <<<HTML
## CSRF校验 id=csrf
CSRF（Cross-site request forgery），中文名称：跨站请求伪造。

### CSRF是什么 id=what-is-csrf
CSRF是一种跨域攻击，我们都知道在浏览器中会将你的登录状态保存在Cookie中，通过Cookie中的信息，你可以在一段时间内免登录访问目标网站，考虑如下情况：
1. 你登录了你们公司的项目管理系统，浏览器就记录了你的登录信息；
2. 你采用`GET`方法请求了后台，删除了一个用户：  
```html
http://hostname/user/delete?userid=1
```
3. 然后你在同一个浏览器中访问了一个危险的网站，这个网站中有如下一条代码：
```html
<img src="http://hostname/user/delete?userid=2"/>
```
4. 这时候你知道应该会发生什么了吧？项目管理系统中的另一个用户也被删除了。

那个危险的网站正是利用了浏览器保留的登录信息，模仿你的行为，发起跨域攻击，为了解决这个问题，针对web页面请求的服务默认都会开启`CSRF`校验。

### 防御CSRF攻击 id=csrf-attack
原理很简单，在用户每次提交请求的时候加入一个包含随机值的字段，这个字段的值在服务端有过备案，这样就可以让服务端知道本次请求来自于自己的前端页面。

在Yii中是这么做的，在`layout/main.php`中有CSRF注册代码，确保每个页面都会被应用：
```php
\$this->registerCsrfMetaTags();
```
这个方法会完成两个任务：
- 每次刷新页面都会生成一个随机字符串；
- 把这个随机字符串保存在服务端session或者cookie中；

开发人员在设计表单的时候应该使用`POST`方法提交数据，并将`_csrf`作为隐藏字段一起提交到后台，由于第三方站点无法获取浏览器其他站点的cookie，无法获取`_csrf`字段，因此可以实现防御跨站点攻击。举例：
```html
<form id="login-form" action="/login" method="post">
    <!-- 添加_csrf隐藏字段，一起提交到服务端，防止第三方网站的csrf攻击 -->
    <input type="hidden" name="_csrf" value="<?=Yii::\$app->getRequest()->getCsrfToken()?>">
    <div class="form-group">
        <label class="control-label" for="username">Username</label>
        <input type="text" id="username" class="form-control" name="LoginForm[username]">
    </div>
    <div class="form-group">
        <label class="control-label" for="password">Password</label>
        <input type="password" id="password" class="form-control" name="LoginForm[password]">
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary" name="login-button">Login</button>
    </div>
</form>
```

基于`yii\web\Controller`类的子类，拥有默认的`CSRF`校验代码：
```php
namespace yii\web;
class Controller extends \yii\base\Controller{
    public function beforeAction(\$action)
    {
        if (parent::beforeAction(\$action)) {
            if (
                \$this->enableCsrfValidation && 
                Yii::\$app->getErrorHandler()->exception === null && 
                !\$this->request->validateCsrfToken()
            ) {
                throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.'));
            }
            return true;
        }
        return false;
    }
}
```
上述的`\$this->request->validateCsrfToken()`方法是校验`CSRF`的主要函数，但是有如下的限制：
- `enableCsrfValidation`必须是true；
- HTTP请求方法不能是GET、HEAD或者OPTIONS；

### 关闭CSRF校验 id=close-csrf
有些朋友设计API的时候喜欢用`yii\web\Controller`这个父类，但是API又不需要`CSRF`校验，因此Yii提供了关闭`CSRF`校验的方法：：
```php
namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public \$enableCsrfValidation = false;
}
```

那我如果想在一个控制器中将某些动作设置为开启`CSRF`校验，对某些动作设置为关闭`CSRF`校验，怎么做呢？因为`CSRF`校验是在父类`yii\web\Controller`的`beforeAction()`中完成的，因此你必须在此之前完成配置，举例如下：
```php
namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{   
    public function actionLogin(){
        // do something
    }
    
    public function actionApi(){
        // do something
    }
    
    public function beforeAction(\$action)
    {
        // 在调用父类的CSRF校验前，根据不同的动作实现不同的配置
        switch(\$action->id){
            case "login":
                // need CSRF Validation
                \$this->enableCsrfValidation = true;
                break;
            case "api":
                // do not need CSRF Validation
                \$this->enableCsrfValidation = false;
                break;
            default:
                break;
        }
        
        // 下面代码才会执行父类的CSRF校验
        return parent::beforeAction(\$action); // TODO: Change the autogenerated stub
    }
}
```

HTML,
    "简单权限控制" => <<<HTML
## 简单权限控制（ACF）  id=acf
在web网站应用中最常见的权限控制机制莫过于基于角色的权限控制机制（RBAC），这种机制很强大，如果你的网站是中大型网站，那它就是首选的权限控制机制，但对于小型网站，用RBAC就有些杀鸡用牛刀的感觉。

为了简化权限控制，Yii给我们提供了存取控制过滤器（ACF），这是一种动作过滤器，非常适用于仅需要简单的存取控制的应用，它通过[yii\\filters\AccessControl](https://www.yiichina.com/doc/api/2.0/yii-filters-accesscontrol) 类来实现简单授权，


> 注意：无论是ACF还是RBAC权限控制机制都是给Web控制器使用的，Console控制器不需要权限控制机制。

`ACF`在控制器中的`behaviors()`方法中设定权限控制规则，以`action`动作的可访问性为目标进行权限控制，如果没有通过权限认证，它会自动跳转到登录页面，举例如下：
```php
use yii\web\Controller;
use yii\\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['login', 'logout', 'signup'],
                'rules' => [
                    [
                        // 允许未登录用户访问本控制器中的`login`和`signup`方法
                        'allow' => true,
                        'actions' => ['login', 'signup'],
                        'roles' => ['?'],
                    ],
                    [
                        // 允许已登录用户访问本控制器中的`logout`方法
                        'allow' => true,
                        'actions' => ['logout'],
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }
    // ...
}
```
- `'only' => ['login', 'logout', 'signup']`：`only`设置权限控制的范围，表示只对`login`、`logout`和`signup`这三个动作生效；
- `'roles' => ['?']`：`?`表示游客，即未登录的用户；
- `'roles' => ['@']`：`@`表示已登录用户；

除了上述这三个规则（`allow`，`actions`，`roles`），还存在如下的规则用于适应不同的情况。

### matchCallback id=matchCallback
[matchCallback](https://www.yiichina.com/doc/api/2.0/yii-filters-accessrule#\$matchCallback-detail)：自定义校验函数，校验成功返回`true`，否则返回`false`，如：

```php
use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['contact'],
                'rules' => [
                    [
                        // 只有在已登录状态下（'@'）的非admin账号才可以访问contact页面
                        'actions' => ['contact'],
                        'allow' => true,
                        'roles' => ['@'],
                        'matchCallback' => function (\$rule, \$action) {
                            return Yii::\$app->user->username != "admin";
                        }
                    ]
            ],
        ];
    }
    // ...
}
```

### verbs id=verbs
[verbs](https://www.yiichina.com/doc/api/2.0/yii-filters-accessrule#\$verbs-detail)：指定该规则用于匹配哪种请求方法（例如GET，POST）。 这里的匹配大小写不敏感。

```php
use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['contact'],
                'rules' => [
                    [
                        'actions' => ['contact'],
                        'allow' => true,
                        'roles' => ['?'],
                        'verbs' => ['get']
                    ]
            ],
        ];
    }
    // ...
}
```
> 注意：只有`GET`方法才可以打开页面，如果上述`verbs`设置为`POST`，则无法打开contact页面，会直接跳转到Login登录页面，因此`verbs`的应用主要体现在页面获取服务端数据的接口中，如：ajax。

Yii还提供另外一种设置`verbs`的方法：
```php
public function behaviors()
{
    return [
        'verbs' => [
            'class' => \yii\filters\VerbFilter::class,
            'actions' => [
                'index'  => ['GET'],
                'view'   => ['GET'],
                'create' => ['GET', 'POST'],
                'update' => ['GET', 'PUT', 'POST'],
                'delete' => ['POST', 'DELETE'],
            ],
        ],
    ];
}
```

### ips id=ips
[ips](https://www.yiichina.com/doc/api/2.0/yii-filters-accessrule#\$ips-detail)：指定该规则用于匹配哪些 客户端IP地址 。 IP 地址可在其末尾包含通配符 \* 以匹配一批前缀相同的IP地址。 例如，`192.168.\*` 匹配所有 `192.168.` 段的IP地址。 如果该选项为空或者不使用该选项，意味着该规则适用于所有角色。

```php
use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['config'],
                'rules' => [
                    [
                        // 表示只有在172.17.0.10电脑上登录admin用户才能访问config配置页面
                        'actions' => ['config'],
                        'allow' => true,
                        'roles' => ['@'],
                        'ips' => ['172.16.0.10'],
                        'matchCallback' => function (\$rule, \$action) {
                            return Yii::\$app->user->username == "admin";
                        }
                    ]
            ],
        ];
    }
    // ...
}
```
上述代码描述了由`config`管理员（172.16.0.10所在的用户）才可以查看和修改系统配置，避免多人同时登录`admin`账号后并发修改配置引起的系统异常，也兼具配置审核的作用。

### 其他 id=other
上述几个配置是`ACF`常用的权限管理选项，其他的选项（`roleParams`，`roles`）是为`RBAC`权限控制机制预留的，以后我们再说。

HTML,
    "controllerMap" => <<<HTML
## 控制器映射（controllerMap） id=controller-map
Yii的路由机制会根据输入的控制器名称来找到同名的控制器，比如说：
```php
// 默认路由机制如下：
'http://hostname/site/contact' => 'frontend\controllers\SiteController'
```
但是控制器映射可以让我们改变这种默认规则，举个例子：
```php
// 配置controllerMap的路由结果
'http://hostname/home/contact' => 'frontend\controllers\SiteController'
```
此时不再是`site`对应`SiteController`，而是`home`对应`SiteController`。要完成这种映射的配置，需要在配置文件中定义映射关系：
```php
return [
    'controllerNamespace' => 'frontend\controllers',
    'controllerMap' => [
        // 用配置数组申明 "home" 控制器
        'home' => [
            'class' => 'frontend\controllers\SiteController',
            //'enableCsrfValidation' => false,
        ],
    ]
];
```
> 注意：`controllerMap`中的`class`命名空间需要和`controllerNamespace`中定义的保持一至。

如果你想映射到`modules`模块里的控制器，就需要在模块中定义：
```php
return [
    'modules' => [
        'yii2' => [
            'class' => 'frontend\modules\yii2\Module',
            
            // 在此处定义模块内的控制器映射关系
            'controllerMap' => [
                'home' => [
                    'class' => 'frontend\modules\yii2\controllers\SiteController',
                    //'enableCsrfValidation' => false,
                ],
            ]
        ],
    ],
];
```

> 注意：控制器映射会改变控制器的id，因此在渲染视图时，会去找以新控制器id为目录的视图文件，如上面配置，会默认去找`views/home/contact.php`这个视图文件。

HTML,
    "response" => <<<HTML
## 修改响应格式（json）
web控制器在默认情况下会以html格式响应用户的请求，但是有时候我们需要用ajax请求数据，此时要求返回的格式应该为json格式：
```php
public function actionAjax(){
    Yii::\$app->response->format = \yii\web\Response::FORMAT_JSON;
    return ['message' => 'hello world'];
}
```
因为json格式较为常用，Yii也提供了便捷方法：
```php
public function actionAjax(){
    return \$this->asJson(['message' => 'hello world']);
}
```

除了Json格式外，Yii还提供了如下响应格式：
```php
const FORMAT_RAW = 'raw';
const FORMAT_HTML = 'html';
const FORMAT_JSON = 'json';
const FORMAT_JSONP = 'jsonp';
const FORMAT_XML = 'xml';
```

HTML

];




echo Markdown::widget(["content" => implode("\n", $html)]);

